package com.schoolComments.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.schoolComments.dto.Result;
import com.schoolComments.entity.RedisShopData;
import com.schoolComments.entity.Shop;
import com.schoolComments.mapper.ShopMapper;
import com.schoolComments.service.IShopService;
import com.schoolComments.utils.SystemConstants;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *  服务实现类
 * </p>
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    ShopMapper shopMapper;
    //过期时间
    private Integer expireTime = 2;
    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    /*
    用存空字符串的方式解决缓存穿透问题
     */
    @Override
    public Result queryById(Long id) {
        //根据id去redis中查数据
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
        //有数据则返回
        //判断数据是否是空字符串
        if(shopJson != null && shopJson.equals("")){
            //空字符串
            return Result.fail("该商铺不存在");
        }
        if (StrUtil.isNotBlank(shopJson)) {
            //非空字符串
            //将json串转为对象
            Shop shopInfo = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shopInfo);
        }
        //没数据
        //去数据库中查数据
        Shop shopInfo = shopMapper.queryShopInfoById(id);
        //没数据则在缓存中写入空串
        if (shopInfo == null) {
            stringRedisTemplate.opsForValue().set("cache:shop:"+id, "", 5, TimeUnit.MINUTES);
            return Result.fail("该商铺不存在");
        }
        //有数据
        //将数据转为Json串
        String jsonStr = JSONUtil.toJsonStr(shopInfo);
        //写入缓存并返回数据
        stringRedisTemplate.opsForValue().set("cache:shop:"+id, jsonStr);
        return Result.ok(shopInfo);
    }

    /*
    用互斥锁解决缓存击穿问题
     */
    public Result queryById2(Long id){
        //根据id去redis中查数据
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
        //有数据则返回
        //判断数据是否是空字符串
        if(shopJson != null && shopJson.equals("")){
            //空字符串
            return Result.fail("该商铺不存在");
        }
        if (StrUtil.isNotBlank(shopJson)) {
            //非空字符串
            //将json串转为对象
            Shop shopInfo = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shopInfo);
        }
        //没数据
        //获得锁
        Shop shopInfo = null;
        try {
            boolean lock = getLock("shop:lock:" + id);
            if (!lock) {
                //没有得到锁
                //过一会重新执行
                try {
                    Thread.sleep(1000 * 5);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return queryById2(id);
            }
            //得到锁
            //double check 查一下缓存
            String cacheShop = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
            if (StrUtil.isNotBlank(cacheShop)) {
                //缓存中有数据则直接返回
                return Result.ok(JSONUtil.toBean(cacheShop, Shop.class));
            }
            //缓存中没有数据
            //去数据库中查数据
            try {
                //假装查询操作很慢
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            shopInfo = shopMapper.queryShopInfoById(id);
            //没数据则在缓存中写入空串
            if (shopInfo == null) {
                stringRedisTemplate.opsForValue().set("cache:shop:"+id, "", 5, TimeUnit.MINUTES);
                return Result.fail("该商铺不存在");
            }
            //有数据
            //将数据转为Json串
            String jsonStr = JSONUtil.toJsonStr(shopInfo);
            //写入缓存并返回数据
            stringRedisTemplate.opsForValue().set("cache:shop:"+id, jsonStr);
        } catch (RuntimeException e) {
            throw new RuntimeException(e);
        }
        finally {
            //释放锁
            unlock("shop:lock:"+id);
        }
        return Result.ok(shopInfo);
    }

    /**
     * 利用 逻辑过期 解决缓存击穿问题
     * @param id
     * @return
     */
    public Result queryById3(Long id){
        //根据id查数据(因为是高频率数据 所以提前准备好放入缓存 所以这里默认是能从缓存中查到数据的)
        String redisShopDataStr = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        //查看TTL是否过期
        RedisShopData redisShopData = JSONUtil.toBean(redisShopDataStr, RedisShopData.class);
        //获取Shop的JSON对象
        JSONObject redisShopData1 = (JSONObject) redisShopData.getData();
        LocalDateTime expireTime = redisShopData.getExpireTime();
        if (!LocalDateTime.now().isAfter(expireTime)) {
            //没过期(返回数据)
            return Result.ok(JSONUtil.toBean(redisShopData1, Shop.class));
        }
        //过期
        //获得锁
        try {
            boolean lock = getLock("shop:lock" + id);
            //没得到(直接返回旧数据)
            if (!lock) {
                return Result.ok(JSONUtil.toBean(redisShopData1, Shop.class));
            }
            //得到
            //double check 查看缓存中TTL是否过期
            String s = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
            RedisShopData redisShopData2 = JSONUtil.toBean(s, RedisShopData.class);
            if(redisShopData2.getExpireTime().isAfter(LocalDateTime.now())){
                //有别的线程加过缓存了 直接返回新数据就行
                return Result.ok(redisShopData2.getData());
            }
            //开一个新线程去数据库查数据
            CACHE_REBUILD_EXECUTOR.submit(() ->{
                //将查到的数据写入缓存
                try {
                    //假装查询慢一点
                    Thread.sleep(1000 * 2);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                GetRedisShopData(id);
            });
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        finally {
            //释放锁
            unlock("shop:lock" + id);
        }
        //当前线程直接返回旧数据
        return Result.ok(JSONUtil.toBean(redisShopData1, Shop.class));
    }

    /**
     * 去数据库中查询得到shop 然后加上逻辑过期时间 并将RedisShopData加入缓存
     * @param id 商铺id
     * @return RedisShopData
     */
    public void GetRedisShopData(Long id){
        Shop shop = shopMapper.queryShopInfoById(id);
        RedisShopData redisShopData = new RedisShopData();
        redisShopData.setData(shop);
        redisShopData.setExpireTime(LocalDateTime.now().plusMinutes(2));
        stringRedisTemplate.opsForValue().set("cache:shop:"+id, JSONUtil.toJsonStr(redisShopData));
    }

    /**
     * 获得锁
     * @param key 键的名字
     * @return  true 成功获得锁
     */
    private boolean getLock(String key){
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
        //先拆箱在返回
        return BooleanUtil.isTrue(aBoolean);
    }

    /**
     * 释放锁
     * @param key 键的名字
     */
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }


    //根据店铺类型查询所有相关店铺
    @Override
    public Result queryShopByType(Integer typeId, Integer current) {
        //开启分页
        PageHelper.startPage(current, SystemConstants.MAX_PAGE_SIZE);
        //执行查询
        List<Shop> shops = shopMapper.queryShopByType(typeId);
        //获取分页信息对象
        PageInfo<Shop> shopPageInfo = new PageInfo<>(shops, current);
        return Result.ok(shopPageInfo.getList());
    }
}
